the minus world explained v2.0 ------------------------------ by doppelganger (doppelheathen@gmail.com) We all know about the supposed "minus world" that "exists" in super mario bros 1 (nes version). Each and every one of us has at one point or another been to it. It's called the minus world because the world display says "-1" instead of "1-1" or "8-1" or some other thing. Nintendo has maintained (and this is definitely true) that it is not anything more than a game glitch, discovered by using the wall-walking glitch clever gamers found and exploited. Many people have blamed the fact that there was no number on the screen currently assigned to the left or right pipe at the time one went down the pipe in world 1-2. This was only a half- assed explanation; the sort that one with no technical knowledge of what was actually going on would believe. The fact that the numbers weren't displayed on the screen at the time one entered the minus world had nothing to do with why the player was actually sent there. Nintendo themselves blamed the wall-walking trick, which was never intended to even be possible, as the reason the minus world "exists" in smb1. Indeed, if you go to the end of world 1-2 on the arcade version of smb1, you'll find that the ceiling bricks that wall- walkers use to reach the minus world have been removed from the pipe leading back up to the surface. But the wall-walking trick was a separate glitch, and had nothing to do with what actually caused the minus world to "exist" in the first place. It was a means, not an end. But then there were some that dug a little deeper and found out just exactly what was going on with this supposed "minus world". This is one of the contributing factors, but not the real precursor behind it all. I'll get into that later, but first I'll review this for those that haven't heard of it: The reason that the left and right pipes always sent you to the minus world if the numbers weren't displayed was because it was actually utilizing the second warp zone (to world 5), and that the left and right pipes were not actually taking you to blank-1, but rather, to world 36-1, which the player was never meant to go to. If one chose the middle pipe, one could actually get to world 5, which was never intended to happen this early in the game. The warp zone numbers are contained in $87f2 of the program rom. The content is as follows: (the numbers with $ are in hexadecimal) ;($87f2) .byte $04, $03, $02, $00 <-- used at the end of world 1-2 .byte $24, $05, $24, $00 <-- used at the end of world 4-2 .byte $08, $07, $06, $00 <-- used in secret area accessible from world 4-2 Now, you'll notice that on the second row, the warp zone number 5 is surrounded on its left and right by $24 (or 36 in decimal). What the game actually uses these numbers for (in addition to figuring out where to send the player) is as tile numbers when the warp zone message "WELCOME TO WARP ZONE!" prints out onto the screen. When the routine prints that message out, it then attempts to get the appropriate warp zone numbers based on the value it's been fed ($04 for 4-3-2, $05 for 36-5-36, and $06 for 8-7-6) by subtracting four and multiplying by four, then using that as the offset to $87f2. Thus $04 would become $00, then stay $00 (0 * 4 = 0), $05 would become $01, then become $04 (1 * 4 = 4), and $06 would become $02. Those numbers multiplied by four could then be used to print the warp zone numbers on the screen. Tiles $00 through $09 just happen to be the numerals 0 through 9, and $24 is the only blank tile in the pattern table used for background. You may ask me, "Where are you headed with this?" Well, you see that next to world 5 on both left and right sides, there is $24/36 in what would be the left and right warp zone number slots. Now, in the normal course of play, when you get to the legitimate warp zone 36-5-36 at the end of world 4-2, you will notice that there is only one pipe in this warp zone, and that it leads to world 5. From this we can infer that the warp zone number 36 was never intended to be used. So when you wall-walk through bricks in world 1-2 and jump down the left or right pipe before the text is displayed, it's actually using the second warp zone instead of the first one, and because there are three pipes at the end of 1-2 instead of just one, it thinks you are going to world 36-1 instead of 4-1 like you should be. Now, this I think is a very good explanation of why the lives display would say "WORLD -1" instead of something legitimate, and why you end up in such a weird place. It does not explain, however, exactly why, if the warp zone is supposed to be set to 4-3-2 in world 1-2, the game uses the second warp zone (36-5-36) before the message "WELCOME TO WARP ZONE!" and the world numbers display, especially so early in the game. ----------------------------------------------------------------- However, I believe I have found the true cause, the ultimate precursor, the start of the whole process, the one unchecked object that created the monster known as minus world: enemy data object $34, the warp zone tag. Now, before I explain exactly what it is that this object does, let's take a look at the end of the level object data and enemy object data for world 1-2: This is a snippet at the end of world 1-2's level data: .byte $6f, $47, $9e, $0f, $0e, $82, $2d, $47, $28, $7a .byte $68, $7a, $a8, $7a, $ae, $01, $de, $0f, $6d, $c5 .byte $fd And this is a smaller snippet from the end of world 1-2's enemy data: .byte $a1, $26, $a9, $26 .byte $ee, $25, $0b .byte $27, $b4 .byte $ff There is of course more data preceding both of these sets, but it's not important for our purposes here. Anyway, this is how the level data is interpreted by the game engine: .byte $6f, $47 <-- (column 6, row 15) sideways exit pipe, height 7 .byte $9e, $0f <-- (column 9, row 14) alter terrain (all solid) .byte $0e, $82 <-- (column 0, row 14) alter terrain (floor 2, ceiling 1), page select .byte $2d, $47 <-- (column 2, row 13) scroll lock (normal) .byte $28, $7a <-- (column 2, row 8) vertical pipe (usable, height 3) .byte $68, $7a <-- (column 6, row 8) vertical pipe (usable, height 3) .byte $a8, $7a <-- (column 10, row 8) vertical pipe (usable, height 3) .byte $ae, $01 <-- (column 10, row 14) alter terrain (floor 2, ceiling 0) .byte $de, $0f <-- (column 13, row 14) alter terrain (all solid) .byte $6d, $c5 <-- (column 6, row 13) scroll lock (warp zone), page select .byte $fd <-- END OF LEVEL DATA The enemies are stored in another set of data, which is much shorter, but anyway this is how the enemy data is read by the game engine: .byte $a1, $26 <-- (X = $a0, Y = $10) platform (upwards) .byte $a9, $26 <-- (X = $a0, Y = $90) platform (upwards) .byte $ee, $25, $0b <-- (X = $e0, Y = $e0) area change (area $25, page $0b if on world 1) .byte $27, $b4 <-- (X = $20, Y = $70) warp zone object, page select .byte $ff <-- END OF ENEMY DATA Now, the relevant objects we need to worry about here as far as the warp zone is concerned are the normal and warp zone scroll locks in the level data, and the warp zone object in the enemy data. The area change object is used to send the player back to the surface if they enter the sideways exit pipe, but it doesn't actually relate to the minus world except if you want to understand why the minus world infinitely loops, so I won't explain how it works until then. Now, when the game parses the normal scroll lock object, it sets a flag and prevents all further horizontal scrolling...or at least in other areas it does. However, it parses the warp zone object earlier, and the code for it is as follows: lda $0723 ;check for scroll lock flag beq $b7a3 ;branch if not set to leave lda $ce ;check to see if player's vertical coordinate has and $b5 ;same bits set as in vertical high byte bne $b7a3 ;if so, branch to leave sta $0723 ;otherwise nullify scroll lock flag inc $06d6 ;increment warp zone flag to make warp pipes for warp zone jmp erase_enemy_object ;kill this object For the vast majority of you not familiar with 6502 assembly language instructions, I won't expect you to understand precisely what the code does even with the added comments. I will explain more carefully here. The first two instructions check to see if, for any reason, the scroll lock flag that stops scrolling, has been set. If the scroll flag has not been set, the branch is taken elsewhere and the rest of the instructions are not followed. If it is set, the program continues. Now, these next three instructions are, as far as I can tell, a half-assed attempt to determine if the player is at the top of the screen. However, it doesn't do its job very well, and the player has in fact a 50-50 chance every frame (1/60th of a second if playing the NTSC version of smb1, 1/50 for the PAL) of passing through without branching off elsewhere. At any rate, the test is about as watertight as a sieve and execution continues sooner or later. After that, the game clears the scroll lock flag, allowing the screen to continue scrolling to the right. The warp zone object, being an enemy object, remains in the buffer until removed by some means, so it is still in memory when the normal scroll lock object is reached. The screen doesn't seem to stop scrolling as a result. It is because warp zone object $34 even exists that the level is allowed to continue to scroll, otherwise the scroll lock object would stop the screen from moving after the sideways exit pipe. The last two instructions are simple enough. After restoring free scroll, the game increments the variable that it uses to determine which warp zone is available, which is $06d6. Only one warp zone can ever be available at any time, if at all. The warp zone variable ($06d6) is normally set to zero until a warp zone is found. But when the warp zone object code runs, the variable $06d6 increments and retains the value $01. It then executes the last instruction, which tells it to jump to a subroutine that eliminates the warp zone object from memory. Now I believe the original purpose behind this was to prevent the pipes intended for use as warp zone pipes from sending the player back to the surface (as the sideways pipe would have done, because of the area change object). (I will discuss how warp zones actually work later on.) However, the game never actually intended for the player to be able to reach the pipes until the warp zone message "WELCOME TO WARP ZONE!" was displayed along with the world numbers. Which brings me to the warp zone scroll lock object. This is the code that the warp zone scroll lock actually executes: ldx #$04 ;load value of 4 for game text routine as default lda $075f ;warp zone (4-3-2), then check world number beq wrpz inx ;if world number > 1, increment for next warp zone (5) ldy $074e ;check world type dey bne wrpz ;if ground level type, increment for last warp zone inx ;(8-7-6) and move on wrpz txa sta $06d6 ;store number here for warp zone text and game routines ... ;more code follows, but only this part is relevant First, the game loads the value of $04, which signifies the first warp zone (4-3-2), to be used. Then it checks the world number the player is at. If the player is on world 1, it uses $04 and stores it in the warp zone variable $06d6 I mentioned earlier. If not, the game loads the value $05. It then checks to see what type of level the player is on. If the player is on an underground, water, or castle-type level, it uses the value $05 and stores it in the warp zone variable $06d6. If, however, the player is on a normal ground level, it stores the value of $06. Whichever value the variable $06d6 ends up with is used in the subroutine that prints out the game text to print the correct world numbers above each pipe, and is also the value used (after ANDing out $04), when the player actually uses one of the warp zone pipes, to figure out where to send the player. Elsewhere in the code the scroll lock flag is set and the text subroutine is run. But it is only when the warp zone scroll lock object is parsed that the correct warp zone is set in the variable $06d6. The values $04-$06 are actually flags to the text subroutine that warp zone text is being printed. After it prints out "WELCOME TO WARP ZONE!" it subtracts four in the text routine and multiplies the result by four, and uses that as the offset to get the appropriate warp zone world numbers. Thus $04 becomes $00 which remains $00, $05 becomes $01 which then becomes $04, and $06 becomes $02 which then becomes $08. Now, normally when a player reaches the warp zone as the level designers originally intended, everything goes smoothly. But when a player wall-walks through the solid brick at the end of world 1-2, the warp zone variable $06d6 has already been fed the value $01 because the warp zone object was encountered earlier. It would normally obtain this value from the value $05 after reaching it at the end of world 4-2, thus causing the second warp zone 36-5-36 to be utilized; however, since the warp zone object in enemy data was parsed, the value $01 is in the warp zone variable, thus the second warp zone is utilized. The player can reach the left, middle, or even right pipes without the warp zone text ever displaying by wall-walking, and as long as the player does not move to the right all the way, the warp zone scroll lock is not parsed, thus the variable does not get a chance to update to the appropriate number, which is $04. Now, if the player were to go down the middle pipe, they would, of course, end up in world 5-1 far earlier than the game designers intended. But if they go down the left or right pipes, the game sends them to what it thinks is world 36-1, but since tile #36/$24 is actually a blank tile, the display shows " -1" in the world display, thus the name "minus world". -------------------------------------------------------------------- This is the code actually "runs" the warp zone and decides where the player ends up. It is run whenever the player goes down a pipe: ;($df06) lda $06d6 ;check warp zone control beq $df4a ;branch to leave if none found and #$03 ;mask out all but 2 LSB asl asl ;multiply by four tax ;save as offset to warp zone numbers (starts at left pipe) lda $86 ;get player's horizontal position cmp #$60 bcc $df22 ;if player at left, not near middle, use offset and skip ahead inx ;otherwise increment for middle pipe cmp #$a0 bcc $df22 ;if player at middle, but not too far right, use offset and skip inx ;otherwise increment for last pipe ;($df22) ldy $87f2,x ;get warp zone numbers dey ;decrement for use as world number sty $075f ;store as world number and offset ldx $9cb4,y ;get offset to where this world's area offsets are lda $9cbc,x ;get area offset based on world offset sta $0750 ;store area offset here to be used to change areas ... ;more code follows, but only this part is relevant The first two instructions relate to the warp zone object #34 I discussed at great length earlier. The warp zone control variable $06d6 serves two purposes here: both as the control variable to tell the game which warp zone should be utilized, and as a flag to tell the game to use this warp zone algorithm. (Normally when a player goes down a usable pipe, the warp zone variable $06d6 is set to zero, thus, the player ends up wherever the area offset variable $0750 is set to.) Anyway, the first two instructions check to see if a value exists in warp zone variable $06d6. If it's set to zero, it will skip ahead and leave this code. If it has any other value, however, the code will continue. The warp zone object #34 incremented $06d6 to 1, which causes this code to be executed as a result, whenever the player wall-walks through the end, to the warp zone and goes down the pipes. Had it remained at 0, the player would have ended up back on the surface as if he had entered the sideways exit pipe! (I will explain this more later on when I discuss the science behind minus world's looping.) The next four instructions essentially subtract 4 from the value in $06d6 by clearing all but the first two bits %00000011 in the value in $06d6. Then the result is multiplied by four, and used as the offset to the warp zone numbers stored in $87f2 which I mentioned near the beginning of this document. The offset that it ends up with, however, is only valid for the left pipe. Thus the next set of instructions check the player's horizontal position. If the player is near enough to the left pipe (or at least not that close to the middle, it jumps ahead and uses that number as the offset. If, however, the player is closer to the center, it increments the offset, thus making it valid for the middle pipe. It then checks the player's horizontal position one last time and if the player is closer to the right pipe, the offset is incremented one last time, making it appropriate for the rightmost pipe. Now you might be confused here. Let me just point out again that this code is ONLY executed when the player actually goes down one of the pipes, which locks the player into a horizontal position, so it can make its comparisons. The last bit of code here retrieves the warp zone number based on the offset we had gotten earlier. It decrements it (because the actual numbers it uses are 0-7 for worlds 1-8). It then uses that to find the appropriate area offset for its -1 area to be loaded into the area offset variable $0750 and loads it there. Now, under normal gameplay conditions, when the text for the warp zone displays, the warp zone variable $06d6 has already been fed a value between $04 and $06 that is appropriate, the method of which I discussed earlier. But, I reiterate, when a player wall-walks, and travels down one of the pipes before the text for the warp zone gets a chance to display, the game uses whatever value happens to be in $06d6 at the time, which is $01 because of the warp zone object #34. The instruction "and #$03" has no effect on this number, and it continues on thinking the second warp zone should be utilized. When the value 36/$24 is used as the world number, this code uses the number $24 as the offset, and gets the number $33, which tells it to look exactly $33 bytes past the start of where the area address offsets, which happens to have the byte value $01. The problem is that the look-up table it uses to find the addresses to its level and enemy data is only 36, or $24 bytes long, and $33 bytes past the starting byte is actually outside of the area offset lookup table, which puts it in the enemy data address table. The value $01, which is equivalent to world 2-2/7-2, is actually retrieved from data it's not supposed to access! So the game loads what it thinks is world 36-1, but behaves more like world 7-2. As a side note, the secondary hard mode flag is turned on because the world number exceeds 5. ------------------------------------------------------------- Now, in order to figure out why the minus world loops, we must first understand the area change object in the enemy data and how it works. You'll notice, if we go back to the listing at the end of world 1-2, that the area change object, unlike the other objects in the enemy data, is three bytes long. The first byte, $ee, is the location byte, telling the game where the object is. The second byte, $25, is the area address offset, which happens to be identical to world 1-1's area address offset. This tells the game which area to send the player if he goes down a pipe or climbs a vine (or falls down a hole in one of the cloud levels). The third byte is divided into two parts, the world number bits and the page number bits. I will get into this right now: $0b = %000 01011 world page number number The page number tells it how many "screens" from the beginning the player is supposed to be to the right of when he enters the area. The world number (0-7 for worlds 1-8) tells the game what world the player needs to be in, in order for the object to actually take effect. In this particular case, it is set to 0, or world 1. Since the player happens to be in world 1 at the time this object is encountered, it passes the test and thus the second byte, $25, is written to a separate variable which is read from whenever the player enters a pipe or climbs a vine. This object can be overruled by setting the warp zone variable to something other than zero, which is what warp zone object #34 precisely does, but it will only affect pipes that point straight-up (sideways pipes remain unaffected, thus you can still go to the surface, or area $25, even when the warp zone object #34 is parsed). Now, in the enemy data that is normally read whenever the player reaches worlds 2-2 or 7-2, there are two area change objects that are relevant to the course of normal gameplay (there is actually a third area change object, but it is never used due to the fact that world 3 has no pipes leading to area $01). The object's content is as follows: .byte $2e, $25, $2b <--- x=$20, y=$e0, area $25, page 11 if on world 2 .byte $2e, $25, $4b <--- x=$20, y=$e0, area $25, page 11 if on world 3 (world 3-2 does not use this data) .byte $4e, $25, $cb <--- x=$40, y=$e0, area $25, page 11 if on world 7 The world number set in the third byte of each of these objects has a limited range, and is only meant to encompass worlds 1 through 8. Normally, the area offset variable is set to whatever world the player happens to be in. When passing through worlds 2-2 and 7-2 normally, the game's enemy object parser checks each area change object as it comes to it. If the current world number matches, the second byte is stored in the area offset variable. If not, the object is ignored and the area offset variable contents are left alone. When the game tries to load what it thinks is world 36-1, it is actually reading data from area $01, and because the world number is higher than 5, the secondary hard mode flag is set (on world 5-3 onwards), thus 36-1 acts like world 7-2. However, the world numbers set in the area change objects are not set to 36. In fact, 36 lies far outside the range of $00-$07 the three world number bits allow. Because it is impossible to set the world number in the third byte past world 8 ($07), the game does not load the area offset $25 into the area offset variable (which still contains the value $01 for the world the player is currently in). Because of this, the area offset never changes, and because it never changes, the pipe that would normally lead out to the surface loops back to the beginning of what it thinks is world 36-1. Thus, the minus world loops endlessly. If somehow you've managed to read through this whole thing, I hope you now understand how the minus world works and why it happens. I have tried to make the explanation as easy to understand as possible, but if you have any questions, my e-mail address is at the start of this document. Anyway, have fun, and take care. - doppelheathen